package com.gitee.melin.bee.core.hibernate5;

import com.gitee.melin.bee.core.model.TenantBaseEntity;
import com.gitee.melin.bee.core.support.Pagination;
import com.gitee.melin.bee.core.support.PaginationRequest;
import com.gitee.melin.bee.util.SecurityUtils;
import com.google.common.collect.Lists;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import javax.persistence.TypedQuery;
import org.hibernate.*;
import org.hibernate.criterion.*;
import org.hibernate.engine.jdbc.LobCreator;
import org.hibernate.exception.GenericJDBCException;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.transform.AliasToBeanResultTransformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.orm.hibernate5.SessionFactoryUtils;
import org.springframework.util.ClassUtils;

/**
 * @author admin@gmail.com
 */
@SuppressWarnings({"unchecked", "rawtypes"})
public class HibernateBaseDaoImpl<T, ID extends Serializable> implements HibernateBaseDao<T, ID> {
    private static final Logger logger = LoggerFactory.getLogger(HibernateBaseDaoImpl.class);

    private static final SQLExceptionTranslator DEFAULT_JDBC_EXCEPTION_TRANSLATOR =
            new SQLErrorCodeSQLExceptionTranslator();

    private SQLExceptionTranslator jdbcExceptionTranslator;

    private SQLExceptionTranslator defaultJdbcExceptionTranslator = DEFAULT_JDBC_EXCEPTION_TRANSLATOR;

    @Autowired
    protected ApplicationContext applicationContext;

    @Autowired
    private SessionFactory sessionFactory;

    protected Class<T> entityClass;

    protected String entityName;

    private boolean checkWriteOperations = true;

    private boolean cacheQueries = false;

    private String queryCacheRegion;

    private int fetchSize = 0;

    private int maxResults = 0;

    private boolean tenantClass = false;

    @PostConstruct
    public void postConstruct() {
        Type type = getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            entityClass = (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0];
        }

        tenantClass = ClassUtils.isAssignable(TenantBaseEntity.class, entityClass);

        entityName = sessionFactory.getMetamodel().entity(entityClass).getName();
    }

    @Override
    public LobCreator getLobCreator() {
        return Hibernate.getLobCreator(sessionFactory.getCurrentSession());
    }

    // -------------------------------------------------------------------------
    // Convenience methods for loading individual objects
    // -------------------------------------------------------------------------

    @Override
    public T get(final ID id) {
        return doExecute(session -> {
            if (tenantClass) {
                return queryByNamedParam("id", id);
            } else {
                return (T) session.get(entityClass, id);
            }
        });
    }

    @Override
    public T get(final ID id, final LockOptions lockOption) {
        return doExecute(session -> (T) session.get(entityClass, id, lockOption));
    }

    @Override
    public T load(final ID id) {
        return doExecute(session -> (T) session.load(entityClass, id));
    }

    @Override
    public T load(final ID id, final LockOptions lockOption) {
        return doExecute(session -> (T) session.load(entityClass, id, lockOption));
    }

    @Override
    public List<T> loadAll() {
        return doExecute((HibernateCallback<List<T>>) session -> {
            Criteria criteria = session.createCriteria(entityClass);
            criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
            prepareCriteria(criteria);
            return criteria.list();
        });
    }

    @Override
    public void load(final T entity, final ID id) {
        doExecute((HibernateCallback<T>) session -> {
            session.load(entity, id);

            return null;
        });
    }

    @Override
    public void refresh(final T entity) {
        refresh(entity, null);
    }

    @Override
    public void refresh(final T entity, final LockOptions lockOption) {
        doExecute((HibernateCallback<T>) session -> {
            if (lockOption == null) {
                session.refresh(entity);
            } else {
                session.refresh(entity, lockOption);
            }

            return null;
        });
    }

    @Override
    public boolean contains(final T entity) {
        return doExecute(session -> session.contains(entity));
    }

    @Override
    public void evict(final T entity) {
        doExecute((HibernateCallback<Boolean>) session -> {
            session.evict(entity);
            return null;
        });
    }

    @Override
    public void initialize(T proxy) {
        Hibernate.initialize(proxy);
    }

    @Override
    public Serializable getIdentifierObject(T entity) {
        if (entity == null) {
            logger.warn("Unable to determine the identifier for an empty object");
            return null;
        }
        ClassMetadata cm = this.sessionFactory.getClassMetadata(entityClass);
        if (cm == null) {
            throw new RuntimeException("gIO(): Unable to get class metadata for " + entityClass.getSimpleName());
        }
        return cm.getIdentifier(entity);
    }

    // -------------------------------------------------------------------------
    // Convenience methods for storing individual objects
    // -------------------------------------------------------------------------

    @Override
    public void lock(final T entity, final LockOptions lockOption) {
        doExecute((HibernateCallback<Boolean>) session -> {
            session.buildLockRequest(lockOption).lock(entity);
            return null;
        });
    }

    private void setTenantId(T entity) {
        if (entity instanceof TenantBaseEntity
                && ((TenantBaseEntity) entity).getTenantId() == null
                && SecurityUtils.tenantId() != null) {
            ((TenantBaseEntity) entity).setTenantId(SecurityUtils.tenantId());
        }
    }

    @Override
    public ID save(final T entity) {
        return doExecute(session -> {
            checkWriteOperationAllowed(session);
            setTenantId(entity);
            return (ID) session.save(entity);
        });
    }

    @Override
    public void update(T entity) {
        this.update(entity, null);
    }

    @Override
    public void update(final T entity, final LockOptions lockOption) {
        doExecute((HibernateCallback<T>) session -> {
            checkWriteOperationAllowed(session);
            setTenantId(entity);
            session.update(entity);
            if (lockOption != null) {
                session.buildLockRequest(lockOption).lock(entity);
            }
            return null;
        });
    }

    @Override
    public void saveOrUpdate(final T entity) {
        doExecute((HibernateCallback<T>) session -> {
            checkWriteOperationAllowed(session);
            setTenantId(entity);
            session.saveOrUpdate(entity);
            return null;
        });
    }

    @Override
    public void replicate(final T entity, final ReplicationMode replicationMode) {
        doExecute((HibernateCallback<T>) session -> {
            checkWriteOperationAllowed(session);
            session.replicate(entity, replicationMode);
            return null;
        });
    }

    @Override
    public void persist(final T entity) {
        doExecute((HibernateCallback<T>) session -> {
            checkWriteOperationAllowed(session);
            session.persist(entity);
            return null;
        });
    }

    @Override
    public T merge(final T entity) {
        return doExecute(session -> {
            checkWriteOperationAllowed(session);
            return (T) session.merge(entity);
        });
    }

    @Override
    public void delete(T entity) {
        setTenantId(entity);
        this.delete(entity, null);
    }

    @Override
    public T delete(ID id) {
        T entity = this.get(id);

        if (entity != null) {
            this.delete(entity);
        }
        return entity;
    }

    @Override
    public void delete(final T entity, final LockOptions lockOption) {
        doExecute((HibernateCallback<T>) session -> {
            checkWriteOperationAllowed(session);
            if (lockOption != null) {
                session.buildLockRequest(lockOption).lock(entity);
            }
            session.delete(entity);
            return null;
        });
    }

    @Override
    public void deleteAll(final Collection<T> entities) {
        doExecute((HibernateCallback<T>) session -> {
            checkWriteOperationAllowed(session);
            for (T entity : entities) {
                setTenantId(entity);
                session.delete(entity);
            }
            return null;
        });
    }

    @Override
    public Integer deleteOrUpdateByHQL(final String hql, final String paramName, final Object value) {
        return this.deleteOrUpdateByHQL(hql, new String[] {paramName}, value);
    }

    @Override
    public Integer deleteOrUpdateByHQL(final String hql, final String[] paramNames, final Object... values) {
        return doExecute(session -> {
            TypedQuery query = session.createQuery(hql);
            for (int i = 0, len = paramNames.length; i < len; i++) {
                query.setParameter(paramNames[i], values[i]);
            }
            return query.executeUpdate();
        });
    }

    @Override
    public void flush() {
        doExecute((HibernateCallback<T>) session -> {
            session.flush();
            return null;
        });
    }

    @Override
    public void clear() {
        doExecute((HibernateCallback<T>) session -> {
            session.clear();
            return null;
        });
    }

    // -------------------------------------------------------------------------
    // Convenience finder methods for HQL strings
    // -------------------------------------------------------------------------

    @Override
    public List findByHQL(final String queryString, final Object... values) {
        return doExecute(session -> {
            Query queryObject = session.createQuery(queryString);
            prepareQuery(queryObject);
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    queryObject.setParameter(i, values[i]);
                }
            }
            return queryObject.list();
        });
    }

    @Override
    public List findByHQLNamedParam(String queryString, String paramName, Object value) {
        return findByHQLNamedParam(queryString, new String[] {paramName}, new Object[] {value});
    }

    @Override
    public List findByHQLNamedParam(final String queryString, final String[] paramNames, final Object[] values) {
        if (paramNames.length != values.length) {
            throw new IllegalArgumentException("Length of paramNames array must match length of values array");
        }

        return doExecute(session -> {
            Query queryObject = session.createQuery(queryString);
            prepareQuery(queryObject);
            for (int i = 0; i < values.length; i++) {
                applyNamedParameterToQuery(queryObject, paramNames[i], values[i]);
            }
            return queryObject.list();
        });
    }

    @Override
    public List findByHQLValueBean(final String queryString, final Object valueBean) {
        return doExecute(session -> {
            Query queryObject = session.createQuery(queryString);
            prepareQuery(queryObject);
            queryObject.setProperties(valueBean);
            return queryObject.list();
        });
    }

    @Override
    public Pagination<T> findPageByHQL(final String rowSql, final String countSql, final int offset, final int limit) {
        return this.findPageByHQL(rowSql, countSql, offset, limit, new String[] {}, new Object[] {});
    }

    @Override
    public Pagination<T> findPageByHQL(
            final String rowSql,
            final String countSql,
            final int offset,
            final int limit,
            final String propertyName,
            final Object value) {
        return this.findPageByHQL(rowSql, countSql, offset, limit, new String[] {propertyName}, new Object[] {value});
    }

    @Override
    public Pagination<T> findPageByHQL(
            final String rowSql,
            final String countSql,
            final int offset,
            final int limit,
            final String[] propertyNames,
            final Object[] values) {

        return doExecute(session -> {
            Query rowQuery = session.createQuery(rowSql).setFirstResult(offset).setMaxResults(limit);
            Query countQuery = session.createQuery(countSql);

            for (int i = 0, len = propertyNames.length; i < len; i++) {
                if (values[i] != null) {
                    rowQuery.setParameter(propertyNames[i], values[i]);
                    countQuery.setParameter(propertyNames[i], values[i]);
                }
            }
            long totalRecords = (Long) countQuery.uniqueResult();
            List items = rowQuery.list();

            double totalPages = Math.ceil(totalRecords * 1d / limit);
            return new Pagination<T>((long) totalPages, offset, limit, totalRecords, items);
        });
    }

    // -------------------------------------------------------------------------
    // Convenience finder methods for dynamic detached criteria
    // -------------------------------------------------------------------------

    @Override
    public List<T> findByNamedParam(String propertyName, Object value) {
        return this.findByNamedParamAndOrder(null, null, new String[] {propertyName}, new Object[] {value});
    }

    @Override
    public List<T> findByNamedParam(Projection projection, String propertyName, Object value) {
        return this.findByNamedParamAndOrder(projection, null, new String[] {propertyName}, new Object[] {value});
    }

    @Override
    public T queryByNamedParam(String propertyName, Object value) {
        List<T> list = this.findByNamedParamAndOrder(null, null, new String[] {propertyName}, new Object[] {value});
        if (list.size() == 0) {
            return null;
        } else if (list.size() > 1) {
            throw new IncorrectResultSizeDataAccessException(1);
        } else {
            return list.get(0);
        }
    }

    @Override
    public List<T> findByNamedParam(String joinEntity, String propertyName, Object value) {
        return this.findByNamedParamAndOrder(
                new String[] {joinEntity}, new String[] {propertyName}, new Object[] {value});
    }

    @Override
    public List<T> findByNamedParamAndOrder(String propertyName, Object value, Order... orders) {
        return this.findByNamedParamAndOrder(null, null, new String[] {propertyName}, new Object[] {value}, orders);
    }

    @Override
    public List<T> findByNamedParamAndOrder(Projection projection, String propertyName, Object value, Order order) {
        return this.findByNamedParamAndOrder(
                projection, null, new String[] {propertyName}, new Object[] {value}, order);
    }

    @Override
    public List<T> findByNamedParamAndOrder(String joinEntity, String propertyName, Object value, Order order) {
        return this.findByNamedParamAndOrder(
                null, new String[] {joinEntity}, new String[] {propertyName}, new Object[] {value}, order);
    }

    @Override
    public List<T> findByNamedParam(String[] propertyNames, Object[] values) {
        return this.findByNamedParamAndOrder(null, null, propertyNames, values);
    }

    @Override
    public List<T> findByNamedParam(Projection projection, String[] propertyNames, Object[] values) {
        return this.findByNamedParamAndOrder(projection, null, propertyNames, values);
    }

    @Override
    public T queryByNamedParam(String[] propertyNames, Object[] values) {
        List<T> list = this.findByNamedParamAndOrder(null, null, propertyNames, values);
        if (list.size() == 0) {
            return null;
        } else if (list.size() > 1) {
            throw new IncorrectResultSizeDataAccessException(1);
        } else {
            return list.get(0);
        }
    }

    @Override
    public List<T> findByNamedParamAndOrder(String[] propertyNames, Object[] values, Order... orders) {
        return this.findByNamedParamAndOrder(null, null, propertyNames, values, orders);
    }

    @Override
    public List<T> findByNamedParamAndOrder(
            Projection projection, String[] propertyNames, Object[] values, Order... orders) {
        return this.findByNamedParamAndOrder(projection, null, propertyNames, values, orders);
    }

    @Override
    public List<T> findByNamedParamAndOrder(
            String[] joinEntitys, String[] propertyNames, Object[] values, Order... orders) {
        return this.findByNamedParamAndOrder(null, joinEntitys, propertyNames, values, orders);
    }

    @Override
    public List<T> findByNamedParamAndOrder(
            Projection projection, String[] joinEntitys, String[] propertyNames, Object[] values, Order... orders) {
        DetachedCriteria criteria = createDetachedCriteria(joinEntitys, propertyNames, values);

        if (orders != null) {
            for (Order order : orders) {
                criteria.addOrder(order);
            }
        }

        return this.findListByCriteria(projection, criteria);
    }

    @Override
    public Pagination<T> findPageByNamedParam(
            String joinEntity, String propertyName, Object value, final int offset, final int limit) {
        return this.findPageByNamedParamAndOrder(
                null,
                new String[] {joinEntity},
                new String[] {propertyName},
                new Object[] {value},
                null,
                offset,
                limit);
    }

    @Override
    public Pagination<T> findPageByNamedParam(String propertyName, Object value, final int offset, final int limit) {
        return this.findPageByNamedParamAndOrder(
                null, null, new String[] {propertyName}, new Object[] {value}, null, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParam(
            Projection projection, String propertyName, Object value, int offset, int limit) {
        return this.findPageByNamedParamAndOrder(
                projection, null, new String[] {propertyName}, new Object[] {value}, null, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParamAndOrder(
            String propertyName, Object value, Order order, final int offset, final int limit) {
        return this.findPageByNamedParamAndOrder(
                null, null, new String[] {propertyName}, new Object[] {value}, new Order[] {order}, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParamAndOrder(
            Projection projection, String propertyName, Object value, Order order, int offset, int limit) {
        return this.findPageByNamedParamAndOrder(
                projection,
                null,
                new String[] {propertyName},
                new Object[] {value},
                new Order[] {order},
                offset,
                limit);
    }

    @Override
    public Pagination<T> findPageByNamedParam(
            String[] propertyNames, Object[] values, final int offset, final int limit) {
        return this.findPageByNamedParamAndOrder(null, null, propertyNames, values, null, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParam(
            Projection projection, String[] propertyNames, Object[] values, int offset, int limit) {
        return this.findPageByNamedParamAndOrder(projection, null, propertyNames, values, null, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParamAndOrder(
            Projection projection, String[] propertyNames, Object[] values, Order[] orders, int offset, int limit) {
        return this.findPageByNamedParamAndOrder(projection, null, propertyNames, values, orders, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParamAndOrder(
            String[] propertyNames, Object[] values, Order[] orders, final int offset, final int limit) {
        return this.findPageByNamedParamAndOrder(null, null, propertyNames, values, orders, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParamAndOrder(
            String[] joinEntitys,
            String[] propertyNames,
            Object[] values,
            final Order[] orders,
            final int offset,
            final int limit) {
        return this.findPageByNamedParamAndOrder(null, joinEntitys, propertyNames, values, orders, offset, limit);
    }

    @Override
    public Pagination<T> findPageByNamedParamAndOrder(
            Projection projection,
            String[] joinEntitys,
            String[] propertyNames,
            Object[] values,
            final Order[] orders,
            final int offset,
            final int limit) {
        final DetachedCriteria criteria = createDetachedCriteria(joinEntitys, propertyNames, values);

        return doExecute(session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            long totalRecords = (Long)
                    executableCriteria.setProjection(Projections.rowCount()).uniqueResult();
            if (totalRecords > 0) {
                executableCriteria.setProjection(null);
                if (orders != null) {
                    for (Order order : orders) {
                        criteria.addOrder(order);
                    }
                }

                if (projection != null) {
                    executableCriteria.setProjection(projection);
                    executableCriteria.setResultTransformer(new AliasToBeanResultTransformer(entityClass));
                }

                List<T> items = executableCriteria
                        .setFirstResult(offset)
                        .setMaxResults(limit)
                        .list();
                double totalPages = Math.ceil(totalRecords * 1d / limit);
                return new Pagination<T>((long) totalPages, offset, limit, totalRecords, items);
            } else {
                return new Pagination<T>(0, offset, limit, totalRecords, Lists.newArrayList());
            }
        });
    }

    @Override
    public Pagination<T> findPage(final PaginationRequest<T> paginationRequest) {
        final DetachedCriteria criteria = createDetachedCriteria(
                paginationRequest.getJoinEntitys(),
                paginationRequest.getPropertyNames(),
                paginationRequest.getValues());
        return doExecute(session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            int offset = paginationRequest.getOffset();
            int limit = paginationRequest.getLimit();
            long totalRecords = (Long)
                    executableCriteria.setProjection(Projections.rowCount()).uniqueResult();
            if (totalRecords > 0) {
                executableCriteria.setProjection(null);
                if (paginationRequest.getOrders() != null) {
                    for (Order order : paginationRequest.getOrders()) {
                        criteria.addOrder(order);
                    }
                }
                List items = executableCriteria
                        .setFirstResult(offset)
                        .setMaxResults(limit)
                        .list();

                double totalPages = Math.ceil(totalRecords * 1d / limit);
                return new Pagination<T>(
                        paginationRequest.getPage(), (long) totalPages, offset, limit, totalRecords, items);
            } else {
                return new Pagination<T>(0, offset, limit, totalRecords, Lists.newArrayList());
            }
        });
    }

    @Override
    public List<T> findByNamedParam(String[] propertyNames, Object[] values, final int offset, final int limit) {
        return this.findByNamedParamAndOrder(propertyNames, values, new Order[] {}, offset, limit);
    }

    @Override
    public List<T> findByNamedParamAndOrder(
            String[] propertyNames, Object[] values, Order[] orders, final int offset, final int limit) {
        final DetachedCriteria criteria = createDetachedCriteria(null, propertyNames, values);
        return doExecute((HibernateCallback<List<T>>) session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            executableCriteria.setProjection(null);
            if (orders != null) {
                for (Order order : orders) {
                    criteria.addOrder(order);
                }
            }

            return executableCriteria
                    .setFirstResult(offset)
                    .setMaxResults(limit)
                    .list();
        });
    }

    // -------------------------------------------------------------------------
    // Convenience finder methods for named queries
    // -------------------------------------------------------------------------

    @Override
    public List findByNamedQuery(final String queryName, final Object... values) {
        return doExecute(session -> {
            Query queryObject = session.getNamedQuery(queryName);
            prepareQuery(queryObject);
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    queryObject.setParameter(i, values[i]);
                }
            }
            return queryObject.list();
        });
    }

    @Override
    public List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) {
        return findByNamedQueryAndNamedParam(queryName, new String[] {paramName}, new Object[] {value});
    }

    @Override
    public List findByNamedQueryAndNamedParam(
            final String queryName, final String[] paramNames, final Object[] values) {
        if (paramNames != null && values != null && paramNames.length != values.length) {
            throw new IllegalArgumentException("Length of paramNames array must match length of values array");
        }
        return doExecute(session -> {
            Query queryObject = session.getNamedQuery(queryName);
            prepareQuery(queryObject);
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    applyNamedParameterToQuery(queryObject, paramNames[i], values[i]);
                }
            }
            return queryObject.list();
        });
    }

    @Override
    public List findByNamedQueryAndValueBean(final String queryName, final Object valueBean) {
        return doExecute(session -> {
            Query queryObject = session.getNamedQuery(queryName);
            prepareQuery(queryObject);
            queryObject.setProperties(valueBean);
            return queryObject.list();
        });
    }

    // -------------------------------------------------------------------------
    // Convenience finder methods for detached criteria
    // -------------------------------------------------------------------------

    @Override
    public List findListByCriteria(final DetachedCriteria criteria) {
        return this.findListByCriteria(null, criteria);
    }

    @Override
    public List findListByCriteria(Projection projection, final DetachedCriteria criteria) {
        return doExecute(session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            if (projection != null) {
                executableCriteria.setProjection(projection);
                executableCriteria.setResultTransformer(new AliasToBeanResultTransformer(entityClass));
            }
            return executableCriteria.list();
        });
    }

    @Override
    public <R> R findByCriteria(DetachedCriteria criteria) {
        List list = this.findListByCriteria(null, criteria);
        if (list.size() == 0) {
            return null;
        } else if (list.size() > 1) {
            throw new IncorrectResultSizeDataAccessException(1);
        } else {
            return (R) list.get(0);
        }
    }

    @Override
    public Long findCountByCriteria(final DetachedCriteria criteria) {
        return doExecute(session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            long totalCount = (Long)
                    executableCriteria.setProjection(Projections.rowCount()).uniqueResult();
            return totalCount;
        });
    }

    @Override
    public Pagination<T> findPageByCriteria(final DetachedCriteria criteria, final int offset, final int limit) {
        return doExecute(session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            long totalRecords = (Long)
                    executableCriteria.setProjection(Projections.rowCount()).uniqueResult();
            executableCriteria.setProjection(null);
            List items = executableCriteria
                    .setFirstResult(offset)
                    .setMaxResults(limit)
                    .list();

            double totalPages = Math.ceil(totalRecords * 1d / limit);
            return new Pagination<T>((long) totalPages, offset, limit, totalRecords, items);
        });
    }

    @Override
    public List<T> findByExample(final T exampleEntity) {
        return doExecute(session -> {
            Criteria executableCriteria = (entityName != null
                    ? session.createCriteria(entityName)
                    : session.createCriteria(exampleEntity.getClass()));
            executableCriteria.add(Example.create(exampleEntity));
            prepareCriteria(executableCriteria);
            return executableCriteria.list();
        });
    }

    @Override
    public Pagination<T> findPageByExample(final int offset, final int limit) {
        return this.findPageAndOrderByExample(null, offset, limit);
    }

    @Override
    public Pagination<T> findPageAndOrderByExample(final Order[] orders, final int offset, final int limit) {
        return doExecute(session -> {
            Criteria executableCriteria = session.createCriteria(entityClass);
            prepareCriteria(executableCriteria);

            long totalRecords = (Long)
                    executableCriteria.setProjection(Projections.rowCount()).uniqueResult();

            if (totalRecords > 0) {
                if (orders != null) {
                    for (Order order : orders) {
                        executableCriteria.addOrder(order);
                    }
                }
                executableCriteria.setProjection(null);
                List<T> items = executableCriteria
                        .setFirstResult(offset)
                        .setMaxResults(limit)
                        .list();

                double totalPages = Math.ceil(totalRecords * 1d / limit);
                return new Pagination<T>((int) totalPages, offset, limit, totalRecords, items);
            } else {
                return new Pagination<T>(0, offset, limit, totalRecords, Lists.newArrayList());
            }
        });
    }

    @Override
    public Long findCountByNamedParam(String propertyName, Object value) {
        return this.findCountByNamedParam(new String[] {propertyName}, new Object[] {value});
    }

    @Override
    public Long findCountByNamedParam(String[] propertyNames, Object[] values) {
        final DetachedCriteria criteria = createDetachedCriteria(null, propertyNames, values);
        return doExecute(session -> {
            Criteria executableCriteria = criteria.getExecutableCriteria(session);
            prepareCriteria(executableCriteria);

            return (Long)
                    executableCriteria.setProjection(Projections.rowCount()).uniqueResult();
        });
    }

    @Override
    public Long findCount() {
        return doExecute(session -> {
            String queryString = "select count(*) from " + entityName;
            Query queryObject = session.createQuery(queryString);

            return (Long) queryObject.uniqueResult();
        });
    }

    // -------------------------------------------------------------------------
    // Convenience query methods for iteration and bulk updates/deletes
    // -------------------------------------------------------------------------

    @Override
    public Iterator iterate(final String queryString, final Object... values) {
        return doExecute(session -> {
            Query queryObject = session.createQuery(queryString);
            prepareQuery(queryObject);
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    queryObject.setParameter(i, values[i]);
                }
            }
            return queryObject.iterate();
        });
    }

    @Override
    public void closeIterator(Iterator it) {
        Hibernate.close(it);
    }

    @Override
    public int bulkUpdate(final String queryString, final Object... values) {
        return doExecute(session -> {
            Query queryObject = session.createQuery(queryString);
            prepareQuery(queryObject);
            if (values != null) {
                for (int i = 0; i < values.length; i++) {
                    queryObject.setParameter(i, values[i]);
                }
            }
            return queryObject.executeUpdate();
        });
    }

    // -------------------------------------------------------------------------
    // Helper methods used by the operations above
    // -------------------------------------------------------------------------
    protected void prepareCriteria(Criteria criteria) {
        if (this.isCacheQueries()) {
            criteria.setCacheable(true);
            if (this.getQueryCacheRegion() != null) {
                criteria.setCacheRegion(this.getQueryCacheRegion());
            }
        }
        if (this.getFetchSize() > 0) {
            criteria.setFetchSize(this.getFetchSize());
        }
        if (this.getMaxResults() > 0) {
            criteria.setMaxResults(this.getMaxResults());
        }
    }

    @Override
    public DetachedCriteria createDetachedCriteria(String[] joinEntitys, String[] propertyNames, Object[] values) {
        if (joinEntitys != null) {
            return createDetachedCriteria(
                    Arrays.asList(joinEntitys), Arrays.asList(propertyNames), Arrays.asList(values));
        } else {
            return createDetachedCriteria(null, Arrays.asList(propertyNames), Arrays.asList(values));
        }
    }

    @Override
    public DetachedCriteria createDetachedCriteria(
            List<String> joinEntitys, List<String> propertyNames, List<Object> values) {
        DetachedCriteria criteria = DetachedCriteria.forClass(entityClass);

        if (joinEntitys != null) {
            for (String joinEntity : joinEntitys) {
                criteria.setFetchMode(joinEntity, FetchMode.JOIN);
                criteria.createAlias(joinEntity, joinEntity);
            }
        }

        for (int i = 0, len = propertyNames.size(); i < len; i++) {
            String propertyName = propertyNames.get(i);
            Object value = values.get(i);

            if (value == null) {
                throw new IllegalArgumentException(propertyName + " value is null");
            }

            if (value instanceof Criterion) {
                criteria.add((Criterion) value);
            } else if (value instanceof Collection) {
                criteria.add(Restrictions.in(propertyName, (Collection) value));
            } else if (value.getClass().isArray()) {
                criteria.add(Restrictions.in(propertyName, (Object[]) value));
            } else if (value instanceof Map) {
                Iterator<Entry<String, Object>> iterator =
                        ((Map<String, Object>) value).entrySet().iterator();

                Criterion lhs, rhs;
                Entry<String, Object> entry = iterator.next();
                if ("like".equals(propertyName)) {
                    lhs = Restrictions.like(entry.getKey(), entry.getValue());
                    entry = iterator.next();
                    rhs = Restrictions.like(entry.getKey(), entry.getValue());
                } else {
                    lhs = Restrictions.eq(entry.getKey(), entry.getValue());
                    entry = iterator.next();
                    rhs = Restrictions.eq(entry.getKey(), entry.getValue());
                }
                lhs = Restrictions.or(lhs, rhs);

                while (iterator.hasNext()) {
                    entry = iterator.next();
                    if ("like".equals(propertyName)) {
                        rhs = Restrictions.like(entry.getKey(), entry.getValue());
                    } else {
                        rhs = Restrictions.eq(entry.getKey(), entry.getValue());
                    }
                    lhs = Restrictions.or(lhs, rhs);
                }

                criteria.add(lhs);
            } else {
                criteria.add(Restrictions.eq(propertyName, value));
            }
        }
        return criteria;
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void setEntityClass(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    protected <R> R doExecute(HibernateCallback<R> action) {
        Session session = null;
        try {
            session = sessionFactory.getCurrentSession();

            if (tenantClass) {
                Long tenantId = SecurityUtils.tenantId();
                if (tenantId != null && SecurityUtils.tenantStatus()) {
                    Filter filter = session.enableFilter("tenant_filter");
                    filter.setParameter("currentTenant", SecurityUtils.tenantId());
                    filter.validate();
                } else {
                    session.disableFilter("tenant_filter");
                }
            }

            return action.doInHibernate(session);
        } catch (HibernateException e) {
            throw convertHibernateAccessException(e);
        } catch (SQLException e) {
            throw convertJdbcAccessException(e);
        }
    }

    public DataAccessException convertHibernateAccessException(HibernateException ex) {
        DataAccessException exception = null;
        if (getJdbcExceptionTranslator() != null && ex instanceof JDBCException) {
            exception = convertJdbcAccessException((JDBCException) ex, getJdbcExceptionTranslator());
        } else if (GenericJDBCException.class.equals(ex.getClass())) {
            exception = convertJdbcAccessException((GenericJDBCException) ex, getDefaultJdbcExceptionTranslator());
        }

        if (exception != null) {
            return exception;
        }
        return SessionFactoryUtils.convertHibernateAccessException(ex);
    }

    protected DataAccessException convertJdbcAccessException(JDBCException ex, SQLExceptionTranslator translator) {
        return translator.translate("Hibernate operation: " + ex.getMessage(), ex.getSQL(), ex.getSQLException());
    }

    protected DataAccessException convertJdbcAccessException(SQLException ex) {
        SQLExceptionTranslator translator = getJdbcExceptionTranslator();
        if (translator == null) {
            translator = getDefaultJdbcExceptionTranslator();
        }
        return translator.translate("Hibzernate-related JDBC operation", null, ex);
    }

    protected void checkWriteOperationAllowed(Session session) {
        if (isCheckWriteOperations() && session.isDefaultReadOnly()) {
            throw new InvalidDataAccessApiUsageException(
                    "Write operations are not allowed in read-only mode (FlushMode.MANUAL): "
                            + "Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.");
        }
    }

    protected void prepareQuery(Query queryObject) {
        if (isCacheQueries()) {
            queryObject.setCacheable(true);
            if (getQueryCacheRegion() != null) {
                queryObject.setCacheRegion(getQueryCacheRegion());
            }
        }
        if (getFetchSize() > 0) {
            queryObject.setFetchSize(getFetchSize());
        }
        if (getMaxResults() > 0) {
            queryObject.setMaxResults(getMaxResults());
        }
    }

    protected void applyNamedParameterToQuery(Query queryObject, String paramName, Object value)
            throws HibernateException {

        if (value instanceof Collection) {
            queryObject.setParameterList(paramName, (Collection) value);
        } else if (value instanceof Object[]) {
            queryObject.setParameterList(paramName, (Object[]) value);
        } else {
            queryObject.setParameter(paramName, value);
        }
    }

    public SQLExceptionTranslator getJdbcExceptionTranslator() {
        return jdbcExceptionTranslator;
    }

    public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTranslator) {
        this.jdbcExceptionTranslator = jdbcExceptionTranslator;
    }

    public SQLExceptionTranslator getDefaultJdbcExceptionTranslator() {
        return defaultJdbcExceptionTranslator;
    }

    public void setDefaultJdbcExceptionTranslator(SQLExceptionTranslator defaultJdbcExceptionTranslator) {
        this.defaultJdbcExceptionTranslator = defaultJdbcExceptionTranslator;
    }

    public void setCheckWriteOperations(boolean checkWriteOperations) {
        this.checkWriteOperations = checkWriteOperations;
    }

    public boolean isCheckWriteOperations() {
        return this.checkWriteOperations;
    }

    public void setCacheQueries(boolean cacheQueries) {
        this.cacheQueries = cacheQueries;
    }

    public boolean isCacheQueries() {
        return this.cacheQueries;
    }

    public void setQueryCacheRegion(String queryCacheRegion) {
        this.queryCacheRegion = queryCacheRegion;
    }

    public String getQueryCacheRegion() {
        return this.queryCacheRegion;
    }

    public void setFetchSize(int fetchSize) {
        this.fetchSize = fetchSize;
    }

    public int getFetchSize() {
        return this.fetchSize;
    }

    public void setMaxResults(int maxResults) {
        this.maxResults = maxResults;
    }

    public int getMaxResults() {
        return this.maxResults;
    }
}
